package org.codefilarete.tool.trace;

import javax.annotation.Nonnegative;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;

import org.codefilarete.tool.StringAppender;
import org.codefilarete.tool.Duo;
import org.codefilarete.tool.VisibleForTesting;

/**
 * Class aimed at measuring time between 2 events.
 * Basic implementation and use case : start it (done at instantiation time), then printing through {@link #toString()}.
 * One can reset it with {@link #start()}.
 * 
 * @author Guillaume Mary
 */
public class Chrono {
	
	@VisibleForTesting
	static final int MILLIS_MAX = 1000;
	@VisibleForTesting
	static final int SEC_MAX = 60 * MILLIS_MAX;
	@VisibleForTesting
	static final int MIN_MAX = 60 * SEC_MAX;
	@VisibleForTesting
	static final int H_MAX = 24 * MIN_MAX;

	private enum TimeConstant {
		DAY(H_MAX, "d"),
		HOUR(MIN_MAX, "h"),
		MINUTE(SEC_MAX, "min"),
		SECOND(MILLIS_MAX, "s"),
		MILLIS(1, "ms");
		
		private final int millisCount;
		private final String timeUnit;
		
		TimeConstant(int millisCount, String timeUnit) {
			this.millisCount = millisCount;
			this.timeUnit = timeUnit;
		}
		
		private static final Set<TimeConstant> MILLIS_TIME = Collections.unmodifiableSet(EnumSet.of(MILLIS));
		private static final Set<TimeConstant> SECOND_TIME = Collections.unmodifiableSet(EnumSet.of(SECOND, MILLIS));
		private static final Set<TimeConstant> MINUTE_TIME = Collections.unmodifiableSet(EnumSet.of(MINUTE, SECOND, MILLIS));
		private static final Set<TimeConstant> HOUR_TIME = Collections.unmodifiableSet(EnumSet.of(HOUR, MINUTE, SECOND, MILLIS));
		private static final Set<TimeConstant> DAY_TIME = Collections.unmodifiableSet(EnumSet.of(DAY, HOUR, MINUTE, SECOND, MILLIS));
		
		/**
		 * Gives constants (in decreasing order) necessary to format given milliseconds.
		 * 
		 * @param millis any (non-null) millisecond
		 * @return a set of {@link TimeConstant} in decreasing order (from the biggest unit to smallest)
		 */
		private static Set<TimeConstant> getTimeConstantsToUse(@Nonnegative long millis) {
			Set<TimeConstant> constantsToUse;
			if (millis < MILLIS_MAX) {
				constantsToUse = TimeConstant.MILLIS_TIME;
			} else if (millis < SEC_MAX) {
				constantsToUse = TimeConstant.SECOND_TIME;
			} else if (millis < MIN_MAX) {
				constantsToUse = TimeConstant.MINUTE_TIME;
			} else if (millis < H_MAX) {
				constantsToUse = TimeConstant.HOUR_TIME;
			} else {
				constantsToUse = TimeConstant.DAY_TIME;
			}
			return constantsToUse;
		}
	
	}
	
	/**
	 * Formats an instant such as "1:02min 24s 103ms" (each value is optional)
	 * 
	 * @param millis any (non-null) millisecond
	 * @return a printable representation of {@code millis} 
	 */
	public static String format(long millis) {
		if (millis == 0) {	// corner case not handled by algorithm below
			return "0ms";
		}
		StringAppender result = new StringAppender();
		Set<TimeConstant> constantsToUse = TimeConstant.getTimeConstantsToUse(millis);
		for (TimeConstant timeConstant : constantsToUse) {
			Duo<Long, Long> divide = divide(millis, timeConstant.millisCount);
			long quotient = divide.getLeft();
			// we print only what's necessary
			result.catIf(quotient != 0, Long.toString(quotient), timeConstant.timeUnit, " ");
			millis = divide.getRight();
		}
		// removing last space before returning
		return result.cutTail(1).toString();
	}
	
	private static Duo<Long, Long> divide(long millis, int divisor) {
		return new Duo<>(millis / divisor, millis%divisor);
	}
	
	/**
	 * Gives current instant
	 * 
	 * @return current time in millisecond (see {@link System#currentTimeMillis()}
	 */
	public static long now() {
		return System.currentTimeMillis();
	}

	private long startTime;
	
	/** Creates a new instance that starts immediately */
	public Chrono () {
		start();
	}
	
	/**
	 * Returns start time
	 * @return start time (in milliseconds)
	 */
	public long getStartTime() {
		return this.startTime;
	}
	
	/**
	 * Starts this chronometer
	 * @return start time (which means now !)
	 */
	public long start() {
		return startTime = now();
	}
	
	/**
	 * Gives spent time since this chronometer starts
	 * @return the difference between start time and now, in millis
	 */
	public long getElapsedTime() {
		return now() - startTime;
	}
	
	/**
	 * Format spent time since this chronometer's start time
	 * @see Chrono#format
	 * @see Chrono#getElapsedTime
	 */
	@Override
	public String toString() {
		return format(getElapsedTime());
	}
}
